import os
import json
import logging
from pathlib import Path
from urllib.parse import urlparse

from tld import get_tld
from tld.exceptions import TldDomainNotFound
from playwright.sync_api import (
    Browser,
    Dialog,
    Route,
    Frame,
    Request,
    ConsoleMessage,
    TimeoutError as PlaywrightTimeoutError,
)

from .constants import USER_AGENT, BLOCK_DOMAINS


class Checker:

    def __init__(
        self,
        url: str,
        user_agent: str = USER_AGENT,
        nomedia=True,
        block_domains: set[str] = BLOCK_DOMAINS,
    ):
        self.error = None
        self.url = url
        self.user_agent = user_agent
        self.nomedia = nomedia
        self.block_domains = block_domains
        try:
            r = get_tld(url, as_object=True)
            self.maindomain = r.fld
            self.domain = r.fld if not r.subdomain else r.subdomain + "." + r.fld
        except TldDomainNotFound:
            r = urlparse(url)
            self.domain = r.hostname
            self.maindomain = self.domain

        self.token = os.urandom(8).hex()
        self.logger = logging.getLogger(self.domain)
        self.mainjs = self._getmainjs()
        self.done = False

        self.navigate_urls = []
        self.iframe_urls = []
        self.outside_request_urls = set()
        self.inside_request_urls = set()
        self.jsevents = []
        self.htmls = []

    def _getmainjs(self):
        mainjs = Path('main.js').read_text('utf8')
        return mainjs.replace('{{TOKEN}}', self.token)

    def _onconsole(self, c: ConsoleMessage):
        '''console事件,main.js通过dirxml与python通信'''
        if c.type != "dirxml":
            return
        try:
            data = json.loads(c.text)
        except json.JSONDecodeError as e:
            self.logger.warning(
                "onconsole: json decode error %s",
                c.text,
                exc_info=e,
            )
            return
        if data['token'] != self.token:
            self.logger.warning("onconsole: token not match %s", c.text)
            return
        self.logger.info("onconsole: %s", c.text)
        self.jsevents.append({
            "name": data['name'],
            "value": data['value'],
            "url": data['url'],
            "stacks": data['stacks'],
        })

    def _onframenavigated(self, f: Frame):
        '''跳转URL 跳转URL所在页面的URL'''
        if not f.parent_frame:
            # 主页面的跳转
            self.logger.debug("onframenavigated: %s %s", f.url, f.page.url)
            self.navigate_urls.append({
                "navigate_url": f.url,
                "page_url": f.page.url
            })

    def _frameattached(self, f: Frame):
        if f.parent_frame:
            self.iframe_urls.append({
                "iframe_url": f.url,
                "parent_url": f.parent_frame.url
            })

    def _on_route(self, route: Route):
        '''请求事件可以拦截、修改'''
        req: Request = route.request
        url = req.url
        p = urlparse(url)
        domain = p.hostname
        accept = req.headers.get("accept", None)
        if not domain.endswith(self.maindomain):
            self.outside_request_urls.add(url)
        else:
            self.inside_request_urls.add(url)
        # 根据配置判断是否加载媒体资源
        if self.nomedia and accept:
            if accept.startswith("image") or accept.startswith(
                    "video") or accept.startswith("audio"):
                self.logger.debug("on_route: block by nomedia %s", url)
                route.abort('aborted')
                return
        # 域名黑名单
        if domain in self.block_domains:
            self.logger.debug("on_route: block by domain %s", url)
            route.abort('aborted')
            return
        # 通过fetch获取原始返回
        try:
            resp = route.fetch()
        except Exception as e:
            route.abort('aborted')
            self.logger.warning("on_route: fetch error %s", url, exc_info=e)
            return

        content_type = resp.headers.get("content-type", "")
        self.logger.debug("on_route: %s %s %s %s", req.method, req.url,
                          resp.status, content_type)

        # 保存所有html供后续分析
        if content_type.startswith("text/html"):
            body = resp.body()
            self.htmls.append({
                "url": url,
                "code": resp.status,
                "html": body.decode('utf8', 'ignore')
            })
        route.fulfill(response=resp)

    def _ondialog(self, d: Dialog):
        '''关闭弹窗防止阻塞'''
        self.logger.debug("ondialog: %s %s %s", d.message, d.type,
                          d.default_value)
        if self.done and d.type == 'beforeunload':
            d.accept()
        d.dismiss()

    def check(self, browser: Browser, timeout=1000 * 30):
        '''检测目标网址'''
        page = browser.new_page(
            ignore_https_errors=True,
            user_agent=self.user_agent,
        )
        page.add_init_script(path="stealth.min.js")
        page.add_init_script(script=self.mainjs)
        page.route("**/*", self._on_route)
        page.on('framenavigated', self._onframenavigated)
        page.on('frameattached', self._frameattached)
        page.on("dialog", self._ondialog)
        page.on("console", self._onconsole)
        try:
            page.goto(self.url, timeout=timeout)
            page.wait_for_timeout(timeout)
        except Exception as e:
            self.logger.error("error")
            self.error = str(e)
        self.done = True
        page.close()
